良葛格過世的消息對我來說十分衝擊,筆者從國中開始學 C 語言,就是一路看良哥的筆記長大,乃至於後來學的 Java, Python 以及很多軟體設計的思維都受到良哥很大影響。人生短暫而脆弱,我們雖然無法帶走任何東西,但良哥留下的著作卻能夠深深影響我們。他的文字流傳於世,仿佛他仍然活著。有感於此,取之於網路,用之於網路,決定在這裡展開筆者的技術分享之旅。由於筆者過去只有在擔任課堂助教時才有編寫教學講義的經驗,出社會後的歷練也還不夠深厚,如果在文章中有發現不好的地方,請大家多多包涵,並歡迎提出建議來討論。
本文透過簡單的 Python 爬蟲程式,爬取 PTT 的文章標題。筆者其實一直想試試看製作一個文章分類的分類器,在訓練分類器之前,需要先有個訓練資料,而 PTT 論壇就是個爬蟲友善的網站,討論區的資訊量非常豐富,非常適合練習爬蟲。本文以西洽板 (C_Chat) 為例,因為西洽板討論量豐富且沒有年齡限制,在實做上更為友善。
希望可以製作一個資料集,獲得類似以下格式的資料:
[
{"Category": "問題", "Title": "這是什麼神奇寶貝"},
{"Category": "討論", "Title": "有哪些太晶屬性能近乎完美反制啊"},
{"Category": "閒聊", "Title": "SHIROBAKO 白箱聖地巡禮 第四彈"},
...
]
之後我們可以運用這樣的資料集,來嘗試訓練 NLP 模型,例如使用 Title 來預測 Category 是什麼。
首先我們需要安裝 requests
, beautifulsoup4
兩個套件:
pip install requests beautifulsoup4
透過 requests
套件發送 HTTP Request 並取得網頁原始碼
import requests
url = "https://www.ptt.cc/bbs/C_Chat/index.html"
resp = requests.get(url)
# 將結果存在 Source.html 裡面
with open("Source.html", "w", encoding="UTF-8") as f:
f.write(resp.text)
基於爬蟲的道德倫理,可以只爬一次的東西,就不要爬兩次。在練習爬蟲的過程中難免會多次發送 Request,在迴圈裡面使用爬蟲,一個不慎可能會對目標伺服器造成很大的負擔,所以我們最好養成把需要操作的資料先存在本機的習慣。
Beautiful Soup 4 這個 Python 套件可以讓開發者輕鬆分析 HTML, XML 這種標記語言格式的資料,在爬蟲程式這種需要大量操作 HTML 的應用裡面有相當大的用處。接下來用 Beautiful Soup 4 把剛剛存下來的 Source.html
進行分析,並嘗試提取裡面的標題:
from bs4 import BeautifulSoup
with open("Source.html", "r", encoding="UTF-8") as f:
bs = BeautifulSoup(f.read(), features="html.parser")
links = bs.find_all("div", attrs={"class": "title"})
for link in links:
print(link.text.strip())
以上程式碼可能會印出類似以下的結果
[閒聊] 如果虛白是大奶正妹的話,一護會?
[閒聊] 其實當五絕也不是那麼得好。
Re: [閒聊] 世界各國最受歡迎的初代寶可夢
[公告] C_Chat板板規 v.16.8 暨好文補M區
[公告] 看板活動公告彙整 & 置底推文閒聊區
[名人] 批踢踢推廣中心-看板知名人物題目募集中
[公告] 4-11選舉期間從嚴條款
[公告] C_Chat 板 開始舉辦樂透!
接下來我們需要把標題的分類與標題本身切開來,處理並且跳過已被刪除的文章。這段算是相當單純的字串,不熟 Python 的朋友不妨自己實做看看,以下是筆者的做法:
def SplitTitle(title: str):
if "本文已被刪除" in title:
return
if "[" not in title:
return
if "]" not in title:
return
a = title.index("[") + 1
b = title.index("]")
category = title[a:b].strip()
title = title[b + 1 :].strip()
return {"Category": category, "Title": title}
titles = [
"[閒聊] 如果虛白是大奶正妹的話,一護會?",
"[閒聊] 其實當五絕也不是那麼得好。",
"Re: [閒聊] 世界各國最受歡迎的初代寶可夢",
"[公告] C_Chat板板規 v.16.8 暨好文補M區",
"[公告] 看板活動公告彙整 & 置底推文閒聊區",
"[名人] 批踢踢推廣中心-看板知名人物題目募集中",
"[公告] 4-11選舉期間從嚴條款",
"[公告] C_Chat 板 開始舉辦樂透!",
]
for t in titles:
print(SplitTitle(t))
會印出以下結果:
{'Category': '閒聊', 'Title': '如果虛白是大奶正妹的話,一護會?'}
{'Category': '閒聊', 'Title': '其實當五絕也不是那麼得好。'}
{'Category': '閒聊', 'Title': '世界各國最受歡迎的初代寶可夢'}
{'Category': '公告', 'Title': 'C_Chat板板規 v.16.8 暨好文補M區'}
{'Category': '公告', 'Title': '看板活動公告彙整 & 置底推文閒聊區'}
{'Category': '名人', 'Title': '批踢踢推廣中心-看板知名人物題目募集中'}
{'Category': '公告', 'Title': '4-11選舉期間從嚴條款'}
{'Category': '公告', 'Title': 'C_Chat 板 開始舉辦樂透!'}
只有一頁討論列表的標題,資料量顯然是不夠的。為了獲得更多的資料,我們需要造訪討論列表的「上一頁」
透過檢查網頁元件的方法,我們得知「上一頁」是個 class
為 btn wide
的 a
元件
但很不幸的,這裡至少有四個按鈕都是 btn wide
,類似這樣的例子在網路爬蟲裡面其實很常見,需要多多觀察。這個例子,我們只需要簡單判斷元件的文字即可:
from bs4 import BeautifulSoup
with open("Source.html", "r", encoding="UTF-8") as f:
bs = BeautifulSoup(f.read(), features="html.parser")
def FindNextPage(bs):
links = bs.find_all("a", attrs={"class": "btn wide"})
for link in links:
if link.text == "‹ 上頁":
return link.attrs["href"]
print(FindNextPage(bs))
輸出結果如下:
/bbs/C_Chat/index17572.html
這邊抓到的連結只包含網址的後半而已,記得在前面加上 "https://www.ptt.cc"
整合以上步驟,完整的程式碼如下:
import json
import requests
from bs4 import BeautifulSoup as BS
def Main():
base_url = "https://www.ptt.cc"
sub_url = f"/bbs/C_Chat/index.html"
data = list()
for _ in range(1000):
full_url = f"{base_url}{sub_url}"
bs = BS(requests.get(full_url).text, features="lxml")
titles = bs.find_all("div", attrs={"class": "title"})
for title in titles:
title = title.text.strip()
title = SplitTitle(title)
if title is None:
continue
data.append(title)
sub_url = FindNextPage(bs)
with open("Data.json", "w", encoding="UTF-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def SplitTitle(title: str):
if "本文已被刪除" in title:
return
if "[" not in title:
return
if "]" not in title:
return
a = title.index("[") + 1
b = title.index("]")
category = title[a:b].strip()
title = title[b + 1 :].strip()
return {"Category": category, "Title": title}
def FindNextPage(bs):
links = bs.find_all("a", attrs={"class": "btn wide"})
for link in links:
if link.text == "‹ 上頁":
return link.attrs["href"]
if __name__ == "__main__":
Main()
透過學習網路爬蟲,可以瞭解基本的 HTML 架構與觀察方法,結合簡單的文字處理,可以讓爬蟲成為一個強大的資料工具。